#! /usr/bin/env bash

# Copyright 2020, Mark Callow
# SPDX-License-Identifier: Apache-2.0

# Generate release notes from git log.
#
# mkrelnotes [-b] [-c] [-d] [-e] [-l] [-p <preface> [-t] <previous release tag> <release name>
#
# <previous release tag> is the name of the tag identifying the previous
# release. <this release> is the name to be given to the tag for the upcoming
# release. N.B. this tag cannot be created until after ther release notes
# have been generated and checked in. The release notes will include changes
# in the two-dot commit range <previous release tag>..HEAD.
#
# <preface> is inserted between the title and the detailed change list
# that is extracted with git log. It should be used to summarize
# new features and known issues using level 3 (###) headings for those
# sections.
#
# If -b is specified build system changes will be included in the
# release notes.
#
# If -c is specified any previous $RELNOTES_FILE will be appended, with the
# title lines stripped, to the release notes generated here, thus enabling
# cumulative release notes. Otherwise any previous $RELNOTES_FILE file is
# replaced.
#
# If -d is specified details of the changes will be included, otherwise only
# the summary is shown.
#
# If -e is specified changes to `external` packages will be included in the
# release notes.
#
# If -l is specified changes to the load test apps will be included in the
# release notes.
#
# If -i is specified the release notes will be built interactively. That is,
# for each change the user will be asked if it should be included.
#
# If -t is specified changes to the tests will be included in the release notes
# otherwise they are omitted.

# Depth of this script relative to the project root
depth=..

RELNOTES_FILE="RELEASE_NOTES.md"

# The author name in a commit message comes from whatever the user
# has set as user.name in their git config. It usually has no relation
# to their GitHub username so putting an @ before it has no meaning.
#
SUMMARY_FORMAT_W_AUTH="* %s (%h) (%an)"
SUMMARY_FORMAT="* %s (%h)"

function commit_summary() {
  local hash=$1
  local summary
  local prjson
  pr=$(git log --oneline -n 1 $hash | grep -o -E "\(#[0-9]+\)" | grep -o -E "[0-9]+")
  # Even in PR's the commit has the full name of the user as known to GitHub,
  # not their GitHub username. We want to use the GitHub username so there will
  # be a link back to the person. We can retrieve this by requesting detailed
  # info from GitHub via HTTPS.
  summary=$(git log --pretty="$SUMMARY_FORMAT" -n 1 $hash)
  if [ -z "$pr" ]; then
    cmtjson="$(curl --netrc https://api.github.com/repos/KhronosGroup/KTX-Software/commits/$hash 2>/dev/null)"
    author="$(echo $cmtjson | jq -r -e '.author.login')"
  else
    prjson="$(curl --netrc https://api.github.com/repos/KhronosGroup/KTX-Software/pulls/$pr 2>/dev/null)"
    author="$(echo $prjson | jq -r -e '.user.login')"
  fi
  summary+=" (@$author)"
  # \_ avoids confusion with _ for italics.
  echo "$summary" | sed -e 's/_/\\_/g'
  if [ -n "$includeDetails" ]; then
    body="$(git log --pretty="%b" -n 1 $hash | sed -E 's/^(.+)/  \1/')"
    if [ -n "$body" ]; then
      echo ""
      # Remove CR which some multi-line commit bodies contain for unknown
      # reasons.
      echo "$body" | tr -d \\r | sed -e 's/_/\\_/g'
    fi
  fi
}

function revisions_in () {
  local range=$1; shift
  git rev-list $range $*
}

function log() {
  local part=$1; shift
  local log
  for rev in $(revisions_in "$range" $*); do
    if [ -n "$interactive" ]; then
      git log -1 $rev > /dev/tty

      processed=0
      while [ $processed -eq 0 ]; do
        echo "Include this change? [d,i,s,?] ?" > /dev/tty
        read -n 1 opt
        echo "" > /dev/tty

        case $opt in
          d)
            git show --pretty=oneline $rev > /dev/tty
            ;;
          [is])
            processed=1
            ;;
          ?)
            echo "d - show diff of this commit" > /dev/tty
            echo "i - include this commit." > /dev/tty
            echo "s - skip this commit." > /dev/tty
            echo "? - display this help message." > /dev/tty
            ;;
          *)
            echo "Unknown option: $opt. Try again." > /dev/tty
            ;;
        esac
      done

      case $opt in
        i)
          log+="$(commit_summary $rev)"
          ;;
      esac

    else
      log+="$(commit_summary $rev)"
    fi
    # For reasons I do not understand I have been completely unable to prevent
    # trailing newlines from being removed from the output of commit_summary
    # so resorting to ANSI-C quoting to insert some new lines between summaries.
    log+=$'\n\n'
  done
  if [ -n "$log" ]; then
    echo "### $part"
    echo
    echo "$log"
    echo
  fi
}

function usage() {
cat << EOU
Usage: $0 [-b] [-c] [-d] [-i] [-p <preface>] [-t] <previous release tag> <release name>"
Options:
         -b  Include build system changes.
         -c  Make cumulative release notes.
         -d  Include details of changes. If absent only the summary is shown.
         -e  Include changes to `external` packages.
         -i  Interactively select the changes to include.
         -l  Include changes to load test apps. Ignored if -t specified.
         -p <preface>
             Include the file <preface> before the list of changes.
         -t  Include test changes.
EOU
}

while true; do
  case $1 in
    -b) includeBuildSystem="true"; shift ;;
    -c) cumulative="true"; shift;;
    -d) includeDetails="true"; shift ;;
    -e) includeExternal="true"; shift ;;
    -i) interactive="true"; shift;;
    -l) includeLoadtests="true"; shift ;;
    -p) preface=$2; shift 2 ;;
    -t) includeTests="true"; shift ;;
    -*) usage; exit 1 ;;
    *) break ;;
  esac
done

if [ $# -ne 2 ]; then
  echo "$0: Need previous release tag and this release name, e.g. 'v4.0.0 v4.0.1'."
  exit 1
else
  range=$1..
  lastrel=$1
  thisrel=$2
fi

#if [ $# -ne 1 ]; then
#  echo '$0: Need a two-dot revision range, e.g, `v4.0.0..v4.0.1`.'
#  exit 1
#else
#  range=$1
#  if ! [[ $range =~ ([[:alnum:][:punct:]]+)\.\.([[:alnum:]][[:alnum:][:punct:]]*)? ]]; then
#    echo "$0: <revision range> is not a two-dot range."
#    exit 1
#  else
#    lastrel=${BASH_REMATCH[1]}
#    thisrel=${BASH_REMATCH[2]}
#    if [ -z "$lastrel" ]; then
#      lastrel=HEAD
#    fi
#  fi
#fi

# Change to the project root
cd $(dirname $0)/$depth

SAVED_RELNOTES_FILE="${RELNOTES_FILE%.md}-$lastrel.md"

# Save or remove old relnotes.
if [ -f $RELNOTES_FILE ]; then
  if [ -z "$cumulative" ]; then
    rm $RELNOTES_FILE;
  else
    mv $RELNOTES_FILE $SAVED_RELNOTES_FILE
  fi
fi

# Read preface file before cd.
if [ -n "$preface" ]; then
  PREFACE=$(cat $preface)
fi

lib=$(log libktx lib)
tools=$(log Tools tools)
js_binding=$(log "JS Bindings" interface/js_binding)
java_binding=$(log "Java Bindings" interface/java_binding)
python_binding=$(log "Python Bindings" interface/python_binding)

if [ -n "$includeBuildSystem" ]; then
   build=$(log "Build Scripts and CMake files" scripts $(find . \( -path ./build -o -path ./.git -o -path ./external -o -path ./other_include \) -prune -false -o -name CMakeLists.txt -o -name '*.cmake'))
fi
if [ -n "$includeExternal" ]; then
   external=$(log "External Package Dependencies" external)
fi
if [ -n "$includeTests" ]; then
  tests=$(log "Tests" tests)
elif [ -n "$includeLoadtests" ]; then
  loadtests=$(log "Load test apps" tests/loadtests)
fi

cat > $RELNOTES_FILE <<- EOF
$(date '+<!-- Copyright %Y, The Khronos Group Inc. -->')
<!-- SPDX-License-Identifier: Apache-2.0 -->
Release Notes
=============
## Version ${thisrel#v}
$PREFACE

### Changes since $lastrel (by part)
$lib

$tools

$js_binding

$java_binding

$python_binding

$external

$tests

$loadtests

$build
EOF

if [ -f $SAVED_RELNOTES_FILE ]; then
  awk '! /Release Notes/ && ! /======/ && ! /<!-- Copyright/ && ! /<!-- SPDX/ {print}' $SAVED_RELNOTES_FILE >> $RELNOTES_FILE
  rm $SAVED_RELNOTES_FILE
fi
